Docker 基础(三)- network

在 Docker 的世界里,Network(网络)是赋予容器沟通能力的灵魂所在。默认情况下,容器是一个个完全隔离的、孤独的沙盒。如果没有网络,它们既无法被外部用户访问,之间也无法互相通信。在实际的案例中,Docker 网络最核心的就是弄懂它的 4 种原生网络模式(Bridge、Host、None、Container)以及大厂最爱用的自定义网络。


四种原生网络模式

当你启动一个容器时,可以通过 –net 或 –network 参数来指定不同的网络模式。如果不指定,Docker 默认会为你戴上 Bridge 的帽子。

  • Bridge 模式:这是默认的网桥模式。Docker 进程启动时,会在宿主机上创建一个名为 docker0 的虚拟网桥(可以理解为一个虚拟交换机)。每个运行在 Bridge 模式下的容器,都会由 Docker 分配一个独立的内部 IP,并通过一对虚拟网卡(veth pair)连接到 docker0 上。容器间通过这个内网 IP 可以互相通信。如果外界想要访问容器,必须通过 -p 进行端口映射。这就像在宿主机内部建立了一个局域网,容器是局域网里的独立电脑。
  • Host 模式:宿主机模式。容器不会获得独立的 Network Namespace,也就是说,它没有自己独立的网卡和 IP,而是直接共享宿主机的网络栈。容器内应用监听的端口,就是宿主机的端口。不需要 -p 映射,性能极高(因为没有网络地址转换 NAT 的损耗)。但是如果宿主机上已经跑了一个 8080 端口的服务,Host 模式的容器就再也无法启动 8080 端口了。这种网络模式的本质就是容器直接肉身寄生在宿主机上,和宿主机用同一个网卡。
  • None 模式:无网络模式。Docker 会为容器创建独立的网络空间,但是不进行任何网络配置。容器内没有网卡、没有 IP、没有路由,只有一个孤零零的本地环回网络(lo,即 127.0.0.1)。适用于对安全性要求极高的机密计算、离线批处理任务,或者纯粹不需要网络的计算型脚本。这就像是直接把容器的网线给拔了。
  • Container 模式:容器联盟模式。新启动的容器不会创建自己的网络空间,而是指定去共享一个已经存在的容器的网络配置。两个容器共享同一个 IP,同一个端口范围。这两个容器之间可以通过 localhost 直接通信,效率极高。大名鼎鼎的 Kubernetes(K8s)中的 Pod 概念,底层就是基于这种 Container 模式实现的(Pause 容器)。这就好比搭伙过日子,两个容器用同一根网线。


网桥模式的特点

对于 Bridge 模式和自定义的网络模式,类似于一个交换机有一堆接口,每个接口叫 veth,在本地主机和容器内分别创建一个虚拟接口,并让他们彼此联通(这样一对接口叫 veth pair)。每个容器内部也有一块网卡,每个接口叫做 ethxx。宿主机上的每个 veth 匹配某个容器实例内部的 ethxx,两两配对,一一匹配。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# 在宿主机上
$ docker network ls
NETWORK ID NAME DRIVER SCOPE
2133c9ab0010 bridge bridge local
d3385e8df5e5 host host local
c788000c6843 none null local
b165ac73c636 redis-net bridge local

$ ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host noprefixroute
valid_lft forever preferred_lft forever
2: enp0s3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
link/ether 08:00:27:a7:d7:84 brd ff:ff:ff:ff:ff:ff
altname enx080027a7d784
inet 192.168.1.8/24 brd 192.168.1.255 scope global dynamic noprefixroute enp0s3
valid_lft 71660sec preferred_lft 71660sec
inet6 2409:8a00:2453:0:a00:27ff:fea7:d784/64 scope global dynamic noprefixroute
valid_lft 259134sec preferred_lft 172734sec
inet6 fe80::a00:27ff:fea7:d784/64 scope link noprefixroute
valid_lft forever preferred_lft forever
3: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
link/ether 0a:f9:28:09:5d:e6 brd ff:ff:ff:ff:ff:ff
inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
valid_lft forever preferred_lft forever
inet6 fe80::8f9:28ff:fe09:5de6/64 scope link proto kernel_ll
53: vethcc5b914@if2: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master br-b165ac73c636 state UP group default
link/ether ba:f1:43:90:21:38 brd ff:ff:ff:ff:ff:ff link-netnsid 10
inet6 fe80::b8f1:43ff:fe90:2138/64 scope link proto kernel_ll
valid_lft forever preferred_lft forever
65: veth20984f7@if3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master br-b165ac73c636 state UP group default
link/ether b6:05:32:3e:e3:c1 brd ff:ff:ff:ff:ff:ff link-netnsid 11
inet6 fe80::b405:32ff:fe3e:e3c1/64 scope link proto kernel_ll
valid_lft forever preferred_lft forever


# 在某台容器内
[root@280f37bb7527 app]$$ ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
3: eth1@if65: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
link/ether 16:b7:7a:96:2f:54 brd ff:ff:ff:ff:ff:ff link-netnsid 0
inet 172.18.0.10/16 brd 172.18.255.255 scope global eth1
valid_lft forever preferred_lft forever


自定义网络和生产最佳实践

自定义网络默认使用的是桥接网络模式bridge。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 创建自定义网络 redis-net
$ docker network create redis-net
$ docker network ls
NETWORK ID NAME DRIVER SCOPE
2133c9ab0010 bridge bridge local
d3385e8df5e5 host host local
c788000c6843 none null local
b165ac73c636 redis-net bridge local

# 使用自定义网络启动容器
$ docker run -d -p 8081:8080 --network redis-net --name tomcat81 xxx-tomcat8-jdk8
$ docker run -d -p 8082:8080 --network redis-net --name tomcat82 xxx-tomcat8-jdk8

# 测试在 tomcat81 中 ping tomcat82,以及在 tomcat82 中 ping tomcat81 是否可以直接 ping 通,OK!
$$ ping tomcat82

在实际的企业级项目(比如我们之前搭建的 Redis 集群、MySQL 主从、微服务演练)中,几乎没有任何人会去使用默认的 docker0 默认网桥。业界通用规范是:必须为每个项目创建独立的自定义网络。为什么默认的 Bridge(docker0)不受待见呢?原因是:

  • 无法通过容器名实现内部域名解析:在默认的 docker0 状态下,A 容器想访问 B 容器,必须知道 B 的内部 IP(如 172.17.0.3)。但是容器每次重启 IP 都会变!
  • 自定义网络天然自带容器名解析(内嵌 DNS):如果创建了自定义网络,A 容器访问 B 容器时,直接在代码连接串里写 http://容器名:端口 即可,Docker 底层会自动把容器名解析成当前正确的 IP。


自定义网络实战演练

以微服务连接 Redis 为例。我们来模拟一个真实的场景:使用 Spring Boot 容器(my-app)需要连接 Redis 容器(my-redis)。创建一个专属的自定义网络:

1
$ docker network create my-project-net

启动 Redis 容器,加入该网络

1
$ docker run -d --name my-redis --net my-project-net redis:latest

启动 Spring Boot 容器,加入同一个网络

1
$ docker run -d --name my-app --net my-project-net -p 8081:8081 my-springboot-image:v1.0

此时,在你的 Spring Boot application.yml 配置文件中,Redis 的连接地址(host)再也不用写死成烦人的 IP 了,直接写容器名:

1
2
3
4
5
spring:
data:
redis:
host: my-redis # Docker 会自动将其解析为 Redis 容器的当前内网 IP
port: 6379

即使未来 Redis 容器崩溃重启,IP 发生了变化,你的 Java 微服务也完全不需要改配置或重启,网络依然稳如磐石!


Docker 网络常用指令速查

1
2
3
4
5
6
7
8
9
10
$ docker network --help
Usage: docker network COMMAND
Commands:
connect Connect a container to a network
create Create a network
disconnect Disconnect a container from a network
inspect Display detailed information on one or more networks
ls List networks
prune Remove all unused networks
rm Remove one or more networks
  • docker network ls:查看宿主机上当前所有的 Docker 网络。
  • docker network create [网络名]:创建一个新的自定义 Bridge 网络。
  • docker network inspect [网络名]:深度查看某个网络的配置、网段以及目前挂载了哪些容器。
  • docker network connect [网络名] [容器名]:将一个正在运行的容器动态拉入某个新网络中。
  • docker network disconnect [网络名] [容器名]:将容器从某个网络中踢出去。
  • docker network rm [网络名]:删除某个无用的自定义网络。